#!/usr/bin/env ruby

require 'rubygems'
require 'faster_csv'
require 'gosim'

require 'benchmark'
#require 'profile'
#require 'breakpoint'

require 'lib/util'

class Logger #:nodoc:
  @@sim = GoSim::Simulation.instance

  private
    alias old_format_message format_message

    # Ruby 1.8.3 transposed the msg and progname arguments to format_message.
    # We can't test RUBY_VERSION because some distributions don't keep Ruby
    # and its standard library in sync, leading to installations of Ruby 1.8.2
    # with Logger from 1.8.3 and vice versa.
    if method_defined?(:formatter=)
      def format_message(severity, timestamp, progname, msg)
        "#{@@sim.time}: #{msg}\n"
      end
    else
      def format_message(severity, timestamp, msg, progname)
        "#{@@sim.time}: #{msg}\n"
      end
    end
end

ProfileRequest = Struct.new(:src, :profile)
ProfileResponse = Struct.new(:src, :profile)
NeighborRequest = Struct.new(:src)
NeighborResponse = Struct.new(:src, :neighbors)

class Neighbor
  attr_reader :distance, :addr

  def initialize(addr, profile=nil, distance=999999999)
    @addr = addr
    @profile = profile
    @distance = distance
  end
end

$distance_data = {}
class ClusterNode < GoSim::Net::Node
  MAX_LINKS = 20
  CLUSTER_SIZE = 5
  PACKET_LATENCY = 1
  NUM_ITERATIONS = 10

  @@log = Logger.new(STDOUT)
  @@log.level = Logger::FATAL
  @@log.datetime_format = ""
  attr_reader :profile, :nid, :neighbors

  def initialize(profile)
    super()
    @profile = profile

    @neighbors = {}
    set_timeout(0, false) { start_networking }
  end

  def start_networking
    # Send request packets to the original list of neighbors provided by the
    # simulation setup.
    send_packet(@neighbor_ids, NeighborRequest.new(@nid))
    set_timeout(10, true) { iterate }
  end

  def iterate
    #breakpoint
    cluster = @neighbors.sort {|a,b| a[1].distance <=> b[1].distance}.
      slice(0...CLUSTER_SIZE)

    unless cluster.empty?
      query_for_neighbors(cluster.map {|i| i[0]})

      avg = cluster.map {|addr, node| node.distance}.avg

      @@log.error "#{@nid}: #{cluster.size} cluster neighbors with avg distance #{avg}"
      if $distance_data.has_key?(@sim.time)
        $distance_data[@sim.time] << avg
      else
        $distance_data[@sim.time] = [avg]
      end
    end
  end

  def handle_profile_request(pkt)
    handle_profile_response(pkt) # Profile requests contain the src profile also
    send_packet(pkt.src, ProfileResponse.new(@nid, @profile.clone))
    @@log.debug "#{@nid}: sending profile"
  end

  def handle_profile_response(pkt)
    @neighbors[pkt.src] = Neighbor.new(pkt.src, pkt.profile, @profile.distance(pkt.profile))
    @@log.debug "#{@nid} has saved #{pkt.src} as a neighbor"
  end

  def handle_neighbor_request(pkt)
    send_packet(pkt.src, NeighborResponse.new(@nid, @neighbor_ids.clone))
    @@log.debug "#{@nid}: sending neighbor info"
  end

  def handle_neighbor_response(pkt)
    pkt.neighbors.each do |n| 
      send_packet(n, ProfileRequest.new(@nid, @profile.clone)) unless @neighbors.has_key?(n)
    end 
  end

  def query_for_neighbors(nodes)
    @@log.debug("#{@nid} querying #{nodes.size} neighbors")
    send_packet(nodes, NeighborRequest.new(@nid))
  end

  def handle_failed_packet(pkt)
    puts "Failed Packet from #{@nid} to #{pkt.dest}: #{pkt.data.class}"
  end
end


INITIAL_NEIGHBORS = 5

sim = GoSim::Simulation.instance
#sim.log.level = Logger::DEBUG
nodes = []

srand(10)

FasterCSV.foreach("data/clusters.csv", :converters => :float) do |row|
  nodes << ClusterNode.new(Point.new(row[0], row[1]))
end

nodes.each do |node|
  neighbors = {}
  while neighbors.size < INITIAL_NEIGHBORS
    n = nodes[rand(nodes.size)]
    neighbors[n.nid] = n unless node.nid == n.nid
  end
  node.link(neighbors.keys)
end

puts "Beginning simulation...\n"
puts "Simulation Runtime Statistics: \n\n"
Benchmark.bm do |stat|
  stat.report { sim.run(100) }
end

puts "Distance Data Gathered(#{$distance_data.size})..."
# Output distance data
FasterCSV.open("data/distances.csv", "w", {:col_sep => ' '}) do |csv|
  $distance_data.sort.each do |time, data| 
    csv << [time, data.avg, data.std_dev]
  end
end

exit

# Output a graph
dot_file = File.new("data/graph.dot", "w")
dot_file << "digraph Digger {\n" << 
  "\tnode [color=blue];\n" <<
  "\tedge [color=black, style=filled];\n\n"

graph_nodes = {}
nodes.each_with_index do |node, i|
  label = [node.profile.x.to_i, node.profile.y.to_i].join(',')
  name = "node#{i}"
  dot_file << "\t#{name} [label=\"#{label}\"]\n"
  graph_nodes[node] = name
end

CLUSTER_SIZE = 5

nodes.each_with_index do |node, i|
  node.neighbors.values.sort {|a,b| a.distance <=> b.distance}[0...CLUSTER_SIZE].each do |n|
    dot_file << "node#{i}->#{graph_nodes[n.addr]} " << 
    "[weight=#{(100 - n.distance.to_i) * 10}, " << 
    "label=#{100 - n.distance.to_i}]\n"
  end
end

dot_file << "}\n"

dot_file.close
`neato data/graph.dot -Tps > data/cluster_graph.ps`
